/*
 * Created on Feb 8, 2009
 *
 * TODO
 */
package jacky.lanlan.song.web.filter;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;

/**
 * <p style="color:red">
 * 提供类似 Rails 的请求参数转换：
 * <p>
 * 如果请求参数名类似 <b>foo[]=xxx&foo[]=yyy&...</b>，则自动转换为 {@code List<Object>}， 并存放在 {@code
 * HttpServletRequest} 中，属性名为 foo。 <br>
 * 如果请求参数名类似 <b>foo[aaa]=xxx&foo[bbb]=yyy&...</b>，则自动转换为 {@code Map<String,Object>}， 并存放在 {@code
 * HttpServletRequest} 中，属性名为 foo。
 * <p style="font-weight:bold">
 * <i>以上参数若符合下列格式，将被自动转换为对应的类型：</i><br>
 * <ul style="color:green">
 * <li>整数 => Integer</li>
 * <li>浮点数 => Double</li>
 * <li>true/false => Boolean</li>
 * <li>yyyy-MM-dd 或 yyyy/MM/dd 或 yyyy.MM.dd (比如1984-01-25) => Date</li>
 * <li>"l_"开头，用','分割的值 => {@code List<Object>} (递归操作，即元素也会被转换)</li>
 * <li>"m_"开头，用':'分割键值，用','分割Entry => {@code Map<Object,Object>} (递归操作，即元素也会被转换)</li>
 * </ul>
 * <p style="color:red">
 * 完成以上转换后，所有转换后的参数会被放到一个 {@code Map<String,Object>} [参数名 => 转换后的参数值] 中， 并放入 {@code
 * HttpServletRequest} ，属性名为 params。 <br>
 * 注意，{@code params} 里面只有原始的请求参数，<i>没有</i>自动转换的数组和Map。
 * <p>
 * <b> 例如:<br>
 * foo/bar.action?person[age]=25&person[male]=true&person[birthday]=1984-01-25&person[emails]=l_a@b
 * .com,c@d.com <br>
 * => 调用 request.getAttribute("person") <br>
 * => 得到 {@code Map<String,Object>} <br>
 * => toString()后得到 {birthday=Wed Jan 25 00:00:00 CST 1984, emails=[a@b.com, c@d.com], age=25,
 * male=true} </b>
 * <p style="color:red">
 * 针对参数 Scope ，添加了类似于 Rails {@code flash[:foo]} 的 FlashScope，可以在客户端 redirect 时传递值。<br>
 * 注意，该特性默认情况下是关闭的。可以通过设置初始参数 add-flash-scope 为 true 来开启。
 * <ul style="color:green">
 * <li>在 Controller 里面: {@code
 * ((Map)request.getSession().getAttribute("FLASH_SCOPE")).put("foo",value);}</li>
 * <li>在 redirect 目标页面: {@code $FLASH_SCOPE.foo}</li>
 * </ul>
 * 
 * <p style="color:red">
 * 这个Filter还在请求中添加了一些方便的变量，可以在页面引用：
 * <ul style="color:green">
 * <li> {@code $queryString} => 本次请求的参数字符串，保存在request中</li>
 * <li> {@code $root} => ContextPath(应用URL根路径)，保存在request中</li>
 * <li> {@code $paramName} => 每个请求参数，分别保存在request中 <br>
 * 如果有多个同名参数 (比如上面的List形式foo[])，则会重命名为 foo_0 foo_1 foo_2 ...</li>
 * </ul>
 * 
 * <p style="color:red;font-weight:bold">
 * 以透明的方式提供服务，不依赖第三方库，只需要配置该 Filter 即可使用以上特性。<br>
 * 一般情况下可以不考虑性能问题，可以用来处理全部请求 (\*)。<br>
 * 注意，如果有其他 Filter 需要以上特性，需要把这个 Filter 声明在其前面。
 * 
 * @author Jacky.Song
 */
public class RailsLikeParamsFilter implements Filter {

	@Override
	public void destroy() {}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
			ServletException {
		HttpServletRequest http = (HttpServletRequest) request;

		String queryString = this.reverseToQueryString(http.getParameterMap());
		// 这里不用 http.getQueryString()，因为这个方法无法得到用POST方式发来的请求参数

		if (queryString != null) {
			if (MAP_PATTERN.matcher(queryString).matches()) this.parseMapIfPossible(http);
			
			if (ARRAY_PATTERN.matcher(queryString).matches()) this.parseListIfPossible(http);

			this.copyParamsToRequest(http);
		}

		this.addConvinenceAttributes(http);
		
		if (this.enableFlashScope) this.addFlashScopeFeature(http);

		chain.doFilter(request, response);
	}

	// 转换参数映到查询字符串
	private String reverseToQueryString(Map<String, Object> params) {
		if (params == null || params.isEmpty()) return null;
		StringBuilder query = new StringBuilder();
		for (String name : params.keySet()) {
			if (params.get(name).getClass() == String.class) {
				query.append(name);
				query.append('=');
				query.append(params.get(name));
				query.append('&');
			}
			else if (params.get(name).getClass().isArray()) {
				for (String value : (String[]) params.get(name)) {
					query.append(name);
					query.append('=');
					query.append(value);
					query.append('&');
				}
			}
		}
		return query.deleteCharAt(query.length() - 1).toString();
	}

	// 把请求参数拷贝到请求目标页面的forward请求中(这样目标页面可以从请求中得到用户输入值)
	// 加入 params Map
	private void copyParamsToRequest(HttpServletRequest request) {
		Map<String, String[]> input = request.getParameterMap();
		Map<String, Object> params = new HashMap<String, Object>();
		for (String key : input.keySet()) {
			String[] value = input.get(key);
			if (value.length == 1) {
				request.setAttribute(key, value[0]);
				params.put(key, this.convertParamIfPossible(value[0]));
			}
			else {
				for (int i = 0; i < value.length; i++) {
					String newKey = key + "_" + i;
					request.setAttribute(newKey, value[i]);// 分拆成 key_N 这样的名字
					params.put(newKey, this.convertParamIfPossible(value[i]));
				}
			}
		}
		request.setAttribute("params", params);
	}

	
	private static final Map<Pattern, Class<?>> TYPE_MAP = new ConcurrentHashMap<Pattern, Class<?>>(6);
	static {
		TYPE_MAP.put(Pattern.compile("\\d+"), long.class);
		TYPE_MAP.put(Pattern.compile("\\d+\\.\\d+"), double.class);
		TYPE_MAP.put(Pattern.compile("true|false"), boolean.class);
		TYPE_MAP.put(Pattern.compile("\\d{4}[-\\./](0[1-9]|1[0-2])[-\\./](0[1-9]|[12]\\d|3[01])"), Date.class);
		TYPE_MAP.put(Pattern.compile("^(l_)(.+?,)+.+"), List.class);// l_开头，',' 分割
		TYPE_MAP.put(Pattern.compile("^(m_)(.+?:.+?,)+.+"), Map.class);// m_开头，键值 ':' 分割，Entry ',' 分割
	}

	// 尝试转换值，如果不能转换，值不变
	private Object convertParamIfPossible(String value) {
		Class<?> type = null;
		for (Pattern pattern : TYPE_MAP.keySet()) {
			if (pattern.matcher(value).matches()) {
				type = TYPE_MAP.get(pattern);
				break;
			}
		}

		// / 不能转换 ///
		if (type == null) return value;

		// / 基本类型转换 ///
		PropertyEditor converter = PropertyEditorManager.findEditor(type);
		if (converter != null) {
			try {
				converter.setAsText(value);
				return converter.getValue();
			}
			catch (Exception e) {
				return value;// 不能转换，原样返回
			}
		}

		// / Date///
		if (type == Date.class) {
			try {
				return new SimpleDateFormat("yyyy-MM-dd").parse(value.replaceAll("[\\./]", "-"));// 替换 .和/ 为
			}
			catch (ParseException e) {
				return value;// 如果转换错误，原样返回
			}
		}

		// / Collection 递归 ///
		if (type == List.class) {
			String[] values = value.substring(2).split(",");// 去掉开头的l_再split()
			List<Object> list = new ArrayList<Object>(values.length);
			for (String v : values)
				list.add(this.convertParamIfPossible(v));// 递归转换值

			return list;
		}
		else if (type == Map.class) {
			String[] entries = value.substring(2).split(",");// 去掉开头的m_再split()
			Map<Object, Object> map = new LinkedHashMap<Object, Object>(entries.length);
			for (String entry : entries) {
				String[] pair = entry.split(":");// 分割键和值
				map.put(this.convertParamIfPossible(pair[0]), this.convertParamIfPossible(pair[1]));// 递归转换值
			}
			return map;
		}
		
		return value;
	}
	
	// foo[aaa]=xxx&foo[bbb]=yyy&bar[ccc]=ddd&bar[eee]=fff 递归转换
	private void parseMapIfPossible(HttpServletRequest http) {
		Map<String, Map<String, Object>> maps = null;
		for (Object n : http.getParameterMap().keySet()) {
			String name = (String) n;
			if (MAP_PATTERN.matcher(name).matches()) {
				if (maps == null) maps = new HashMap<String, Map<String, Object>>(2); // 一般不会超过2个

				String tempKey = name.substring(0, name.indexOf('['));// 提取参数名
				if (!maps.containsKey(tempKey)) maps.put(tempKey, new HashMap<String, Object>(5));// 每个Map估算平均5个entry
				maps.get(tempKey).put(
						// Map的键是 [ ] 里面的东西
						name.substring(name.indexOf('[') + 1, name.lastIndexOf(']')),
						convertParamIfPossible(http.getParameter(name)));// 转换值
			}
		}

		if (maps != null) for (String key : maps.keySet())
			http.setAttribute(key, maps.get(key));
	}

	// foo[]=xxx&foo[]=yyy&bar[]=ccc&bar[]=ddd 递归转换
	private void parseListIfPossible(HttpServletRequest http) {
		Map<String, List<Object>> lists = null;
		for (Object n : http.getParameterMap().keySet()) {
			String name = (String) n;
			if (ARRAY_PATTERN.matcher(name).matches()) {
				if (lists == null) lists = new HashMap<String, List<Object>>(3);// 一般不会超过3个
				String[] values = (String[]) http.getParameterMap().get(n);// 值列表
				List<Object> list = new ArrayList<Object>(values.length);
				for (String value : values)
					list.add(convertParamIfPossible(value));// 转换值
				lists.put(name.substring(0, name.length() - 2), list);// attribute名字要去掉"[]"
			}
		}

		if (lists != null) {
			for (String key : lists.keySet())
				http.setAttribute(key, lists.get(key));
		}
	}
	
	/*
	 * 添加一些方便的变量，可以在页面引用
	 * 
	 * 1. 本次请求的参数字符串，然后放在request中，页面用${queryString}访问
	 * 
	 * 2. ContextPath，保存在request中，页面用${root}访问
	 * 
	 */
	private void addConvinenceAttributes(HttpServletRequest http) {
		http.setAttribute("queryString", this.reverseToQueryString(http.getParameterMap()));
		if (contextPath == null) {
			contextPath = http.getContextPath();
		}
		http.setAttribute("root", contextPath);
	}

	// 添加类似于 Rails flash[:foo] 的 FlashScope
	private void addFlashScopeFeature(HttpServletRequest http) {
		if (http.getSession().getAttribute("FLASH_SCOPE") == null) {
			http.getSession().setAttribute("FLASH_SCOPE", new HashMap<String, Object>());
		}
		Map<String, Object> flashScope = (Map) http.getSession().getAttribute("FLASH_SCOPE");

		if (!http.getRequestURI().endsWith(".jsp")) return; // 只针对到jsp的请求

		for (String name : new HashSet<String>(flashScope.keySet())) {
			if (name.startsWith(FLASH_SCOPE_PREFIX)) {// 第二次到jsp页面前,经历过1次Action
				// 然后销毁，实现FlashScope语意
				flashScope.remove(name);
				flashScope.remove(// 删除对应的value
						name.replaceFirst("^" + FLASH_SCOPE_PREFIX, "").replaceAll(FLASH_SCOPE_SUFFIX + "$", ""));
			}
			else {
				String exist_key_name = FLASH_SCOPE_PREFIX + name + FLASH_SCOPE_SUFFIX;
				if (!flashScope.containsKey(exist_key_name)) {// 第一次到jsp页面前
					flashScope.put(exist_key_name, true); // 设置值
				}
			}
		}
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		String scopeFlag = filterConfig.getInitParameter("add-flash-scope");
		this.enableFlashScope = (scopeFlag != null && scopeFlag.equals("true"));
	}

	private boolean enableFlashScope;
	
	private static String contextPath;
	
	
	private static final Pattern MAP_PATTERN = Pattern.compile(".+?\\[.+\\].*");

	private static final Pattern ARRAY_PATTERN = Pattern.compile(".+?\\[\\].*");

	private static final String FLASH_SCOPE_PREFIX = "FLASH_SCOPE_ATTRIBUTE_";

	private static final String FLASH_SCOPE_SUFFIX = "_COUNT";

}
